在 Vue 的魔法世界中,我們已經掌握了許多強大的咒語:組件化、狀態管理、路由守衛、國際化...但今天,我們要學習的是最為優雅的魔法之一 - 主題變換術。
想像一下,如果我們的應用程式就像一個魔法師的實驗室,能夠隨著使用者的心情和環境自由變換色彩和氛圍。白天時呈現清新明亮的 Light 模式,夜晚時切換為深邃神秘的 Dark 模式,甚至還能讓使用者自訂喜愛的顏色組合。
這就是今天我們要實作的 主題系統 - 一個基於 CSS 變數的自由變化骨架設定,讓整個應用程式都能隨著主題的變換而呈現不同的視覺體驗。
今天不管事使用者或是工程師都是喜歡有顏色變換(光暗主題切換的場景)
我們可以透過store來管理參數並前端框架的頁面的檔案結構框架講解一遍~!!
在前端開發中,雖然框架沒有強制規定資料夾結構,但業界已經形成了一套成熟的組織方式。這種設計不僅提高了程式碼的可維護性,更讓團隊協作變得更加順暢。
其實不管是react或是vue都沒有強制限定檔案結構怎麼長~除非是特定的config
才會有固定的路徑
不然一般來說工程師都習慣這樣去做前端的檔案結構:
src/
├── App.vue # 應用程式進入點 - 整個應用的根組件
├── main.js # 應用程式啟動入口
├── layouts/ # 佈局組件 - 應用程式的主幹架構
│ └── MainLayout.vue # 主佈局:包含 Header、導覽、主題控制
├── pages/ # 頁面組件 - 各系統的子路由頁面
│ ├── OrderPage.vue # 點餐系統頁面
│ ├── SummaryPage.vue # 結算系統頁面
│ └── LoginPage.vue # 登入系統頁面
├── components/ # 可重複利用的組件
│ ├── OrderForm.vue # 訂單表單組件
│ ├── OptionGroup.vue # 選項群組組件
│ └── ModalHost.vue # 模態框宿主組件
├── stores/ # 狀態管理
│ ├── themeStore.js # 主題狀態
│ └── authStore.js # 認證狀態
├── router/ # 路由配置
│ └── index.js # 路由定義
└── services/ # API 服務
└── orderService.js # 訂單相關 API
關注點分離 (Separation of Concerns)
App.vue
專注於應用程式的整體結構layouts/
負責整體佈局和框架pages/
專注於特定功能頁面components/
提供可重複使用的 UI 組件可維護性 (Maintainability)
可擴展性 (Scalability)
layouts/
資料夾團隊協作 (Team Collaboration)
原本架構:
App.vue (包含所有功能)
├── 導覽列
├── 語系切換
├── 登入登出
└── RouterView
新架構:
App.vue (精簡為 RouterView)
└── MainLayout.vue (主框架)
├── Header (導覽 + 主題控制)
├── RouterView (子頁面)
└── ModalHost/ToastHost
src/
├── layouts/
│ └── MainLayout.vue # 新增:主框架組件
├── stores/
│ └── themeStore.js # 新增:主題狀態管理
└── style.css # 修改:加入 CSS 變數主題系統
我們建立了完整的 CSS 變數主題系統,讓所有 UI 元素都能統一使用變數。這些變數就像魔法世界的基礎符文,定義了應用程式的視覺基調,並能隨著主題的切換而自動變換其魔力(顏色值)。
在我們的主題系統中,定義了核心的色彩變數,它們是整個視覺語言的基石:
Primary(主色) - #111827
(Light) / #ffffff
(Dark)
Secondary(輔色) - #3b82f6
(Light) / #3b82f6
(Dark)
Text Primary(主要文字色) - #111111
(Light) / #ffffff
(Dark)
Text Secondary(次要文字色) - #4b5563
(Light) / #d1d5db
(Dark)
這些 CSS 變數的價值在於它們的語義化命名和自動適應性:
--color-primary
比 --color-blue
更有意義,因為它表達的是「這是最重要的顏色」,而不是「這是藍色」在做light跟dark切換的時候
可以去思考primary,secondary
主色跟輔色
還有文字色
的設定
/* Light 主題預設 - 核心色彩變數 */
:root {
/* Brand - 品牌魔法色 */
--color-primary: #111827; /* 主色:如魔法陣的核心光芒,用於按鈕、強調元素 */
--color-secondary: #3b82f6; /* 輔色:如輔助咒語的微光,用於次要按鈕、高亮 */
/* Text - 文字魔法色 */
--text-primary: #111111; /* 主要文字:如古老卷軸上的主要銘文 */
--text-secondary: #4b5563; /* 次要文字:如註解或次要資訊的符號 */
/* Surface / Border - 表面與邊界魔法色 */
--bg-page: #ffffff; /* 頁面背景:如廣闊的魔法平原 */
--bg-card: #ffffff; /* 卡片背景:如漂浮的魔法石板 */
--border-color: #e5e7eb; /* 邊框顏色:如魔法結界的邊緣 */
/* 通用半徑/陰影/轉場 - 魔法的形狀與動態 */
--radius-xl: 14px;
--shadow-lg: 0 24px 80px rgba(0,0,0,.12);
--transition-fast: .16s ease;
}
/* Dark 主題覆寫 - 暗夜魔法的降臨 */
[data-theme="dark"] {
--color-primary: #e5e7eb; /* 主色變為淺色,適應暗色背景 */
--color-secondary: #60a5fa; /* 輔色變亮,保持藍色系但增加亮度 */
--text-primary: #f0f0f0; /* 主要文字變為柔和的亮色,減少刺眼感 */
--text-secondary: #b0b0b0; /* 次要文字調整為適中的亮度,確保可讀性 */
--bg-page: #1a1a1a; /* 頁面背景調整為較淺的暗色,減少沉重感 */
--bg-card: #2a2a2a; /* 卡片背景與頁面背景保持適當區別 */
--border-color: #404040; /* 邊框顏色配合新的背景色系 */
}
### 重構既有樣式
所有既有的樣式類別都重構為使用 CSS 變數:
```css
.btn {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 10px;
transition: var(--transition-fast);
}
.btn.primary {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.order {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px;
margin: 8px 0;
background: var(--bg-card);
}
export const useThemeStore = defineStore('theme', {
state: () => ({
mode: 'light' // 'light' | 'dark'
}),
actions: {
setMode(mode) {
this.mode = mode
this.applyTheme()
this.saveToStorage()
},
applyTheme() {
const root = document.documentElement
root.dataset.theme = this.mode
}
}
})
<template>
<div class="main-layout">
<!-- Header:包含標題、導覽、主題控制 -->
<header class="header">
<div class="header-content">
<h1 class="app-title">{{ t('app.header') }}</h1>
<nav class="main-nav">
<router-link to="/order">{{ t('nav.order') }}</router-link>
<router-link to="/summary">{{ t('nav.summary') }}</router-link>
</nav>
<div class="header-controls">
<!-- 主題控制區 -->
<div class="theme-controls">
<select @change="handleModeChange">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<!-- 語系切換和登出 -->
<select @change="switchLocale">
<option value="zh-TW">中文</option>
<option value="en-US">English</option>
<option value="ja-JP">日本語</option>
</select>
<button @click="logout">登出</button>
</div>
</div>
</header>
<!-- 主要內容區 -->
<main class="main-content">
<RouterView v-slot="{ Component }">
<Transition name="page" mode="out-in">
<component :is="Component" />
</Transition>
</RouterView>
</main>
<!-- 全域傳送門 -->
<ModalHost />
<ToastHost />
</div>
</template>
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 12px;
}
.main-nav {
order: 2;
justify-content: center;
}
.header-controls {
order: 3;
justify-content: center;
width: 100%;
}
}
const routes = [
{ path: '/', redirect: '/order' },
{ path: '/login', component: LoginPage }, // 獨立頁面,不在 layout 內
{
path: '/',
component: MainLayout, // 主框架
children: [
{ path: 'order', component: OrderPage },
{ path: 'summary', component: SummaryPage, meta: { requiresAdmin: true } },
{ path: 'order/:id', component: OrderDetailPage },
{ path: 'admin/i18n', component: AdminI18nPage, meta: { requiresAdmin: true } },
{ path: 'analytics', component: AnalyticsPage, meta: { requiresAdmin: true } }
]
}
]
/login
不在 layout 內,不顯示導覽列/
自動重導到 /order
需求描述 | 優先級 | 驗收標準 |
---|---|---|
主題模式切換功能 | 高 | 使用者可以切換 Light/Dark 模式,切換後立即生效 |
主題設定持久化 | 高 | 重新整理頁面後主題設定仍然保留 |
全域主題一致性 | 高 | 所有頁面都使用相同的主題設定 |
登入頁面獨立性 | 中 | 登入頁面不顯示主題控制,但登入後主題設定有效 |
響應式主題適配 | 中 | 主題在手機和桌面版都能正常顯示 |
需求描述 | 優先級 | 驗收標準 |
---|---|---|
視覺舒適性 | 高 | 暗色模式背景不刺眼,文字清晰可讀 |
效能要求 | 中 | 主題切換響應時間 < 100ms |
相容性 | 中 | 支援主流瀏覽器 (Chrome, Firefox, Safari) |
可維護性 | 高 | 新增頁面時自動繼承主題系統 |
作為使用者,我希望能夠切換 Light/Dark 模式
作為使用者,我希望在不同頁面間切換時保持一致的視覺體驗
作為使用者,我希望暗色模式舒適且文字清晰可讀
作為管理員,我希望能夠在管理頁面使用相同的主題系統
主題系統的核心在於 CSS 變數的動態切換。當我們改變 documentElement
的 dataset.theme
屬性時,CSS 會自動套用對應的樣式規則。
applyTheme() {
const root = document.documentElement
// 設定主題模式,觸發 CSS 變數的自動切換
// 這會讓 [data-theme="dark"] 選擇器生效
root.dataset.theme = this.mode
}
為什麼這樣設計?
saveToStorage() {
localStorage.setItem('themeState', JSON.stringify({ mode: this.mode }))
}
為什麼使用 localStorage?
onMounted(() => {
// 載入主題設定
themeStore.loadFromStorage()
// 載入後端 i18n 字典
i18nStore.loadServerConfig()
})
為什麼在 onMounted 中載入?
在大型應用程式中,我們經常會遇到以下問題:
<template>
<div class="main-layout">
<!-- 固定的框架元素 -->
<header class="header">
<h1>{{ t('app.header') }}</h1>
<nav class="main-nav">
<router-link to="/order">{{ t('nav.order') }}</router-link>
<router-link to="/summary">{{ t('nav.summary') }}</router-link>
</nav>
<div class="header-controls">
<!-- 主題控制、語系切換、登出按鈕 -->
</div>
</header>
<!-- 動態的頁面內容 -->
<main class="main-content">
<RouterView v-slot="{ Component }">
<Transition name="page" mode="out-in">
<component :is="Component" />
</Transition>
</RouterView>
</main>
</div>
</template>
Layout 的優勢:
pages/
├── OrderPage.vue # 點餐系統
├── SummaryPage.vue # 結算系統
├── LoginPage.vue # 登入系統
└── AnalyticsPage.vue # 分析系統
Pages 的設計原則:
const routes = [
{ path: '/', redirect: '/order' },
{ path: '/login', component: LoginPage }, // 獨立頁面
{
path: '/',
component: MainLayout, // 主框架
children: [
{ path: 'order', component: OrderPage },
{ path: 'summary', component: SummaryPage },
{ path: 'analytics', component: AnalyticsPage }
]
}
]
巢狀路由的優勢:
/order
、/summary
等路徑簡潔明瞭components/
├── OrderForm.vue # 訂單表單(業務組件)
├── OptionGroup.vue # 選項群組(UI組件)
├── ModalHost.vue # 模態框宿主(容器組件)
└── ToastHost.vue # 提示框宿主(容器組件)
組件設計原則:
<!-- 父組件 (OrderPage.vue) -->
<template>
<OrderForm
:drinks="menuStore.drinks"
:disabled="!withinBusinessHours()"
@submit="handleOrderSubmit"
/>
</template>
<!-- 子組件 (OrderForm.vue) -->
<template>
<form @submit.prevent="onSubmit">
<!-- 表單內容 -->
</form>
</template>
<script setup>
const props = defineProps(['drinks', 'disabled'])
const emit = defineEmits(['submit'])
function onSubmit() {
emit('submit', formData)
}
</script>
為什麼這樣設計?
var(--bg-card)
作為背景色var(--bg-page)
背景,hover 時變為 var(--color-secondary)
var(--bg-card)
背景var(--bg-page)
背景和 var(--text-primary)
文字色✅ 進入首頁自動轉到 /order
,並出現在 MainLayout
框架中
✅ /login
不顯示 MainLayout
的導覽與主題控制
✅ 切換 Light/Dark,頁面背景/卡片/文字/按鈕顏色發生變化;刷新後仍保留
✅ 暗色模式高對比度優化:使用深黑背景配純白文字,提供最佳可讀性和視覺體驗
✅ CSS 變數主題適應:Primary、Secondary、Text Primary、Text Secondary 根據主題自動調整
✅ 表單組件完全支援暗色模式:OrderForm、OptionGroup 等所有表單元素都使用 CSS 變數
✅ OrderPage 跑版問題修復:所有 UI 元素在暗色模式下都有一致的視覺風格
✅ 既有功能不受影響(表單驗證、列表、詳情、匯入、i18n、守衛、Modal/Toast)
✅ 無 console error;專案可正常編譯
今天我們成功實作了 Vue 應用程式的主題系統,這不僅僅是一個技術功能,更是一種使用者體驗的魔法。更重要的是,我們深入探討了前端架構設計的核心思想,理解了為什麼業界都採用這樣的資料夾結構。
App.vue
作為整個應用程式的根組件,它的職責是:
layouts/
資料夾存放的是應用程式的「骨架」,它們:
pages/
資料夾對應的是不同的「功能模組」,每個頁面代表一個完整的業務流程:
components/
資料夾存放的是「可重複使用的 UI 組件」:
透過核心 CSS 變數(Primary、Secondary、Text Primary、Text Secondary),我們建立了統一的視覺語言;透過 MainLayout 架構,我們創造了可擴展的框架結構;透過 ThemeStore,我們實現了狀態的持久化管理。
這個主題系統就像是一個魔法師的調色盤,讓我們的應用程式能夠隨著使用者的心情和環境自由變換。特別是在暗色模式的優化上,我們調整了背景色調,讓它不再過於沉重,同時確保文字在暗色背景下依然清晰可讀,提供舒適的視覺體驗。
顏色部分雖然說不是特別好看,但是也算是完整了這個範例的內容!!
大家可以自由去設計屬於自己的色系讓系統更好看~!!
更重要的是,我們徹底解決了表單組件的暗色模式支援問題。從 OrderForm 的輸入框到 OptionGroup 的選項按鈕,從 OrderPage 的匯入區塊到所有 UI 元素,都完美地融入了 CSS 變數主題系統,確保了視覺的一致性和專業性。
此外,我們也解決了按鈕可見性、導覽狀態、營業時間限制等實際問題,讓整個系統在各種情況下都能正常運作。
這種前端架構設計方式之所以被廣泛採用,是因為它解決了軟體開發中的根本問題:
在 Vue 的魔法世界中,我們學會了如何讓應用程式不僅功能強大,更能貼近使用者的心靈。這就是主題魔法的魅力——讓技術與美感完美融合,創造出真正屬於使用者的魔法體驗。
這種架構設計不僅適用於 Vue,也適用於 React、Angular 等其他前端框架。它代表的是軟體工程的最佳實踐,是無數開發者在實際專案中總結出來的智慧結晶。
「在魔法的世界裡,真正的力量不在於控制,而在於適應與變化。主題系統就是這種力量的體現,而良好的架構設計則是這種力量的根基。」 - Vue 魔法師的智慧